iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 6
3

Guards

在函數式編程中,我們總是希望能寫出更富表達能力的函式,儘可能簡化每個 function body 需要處理的內容。Guards 讓你可以在函式的第一行,快速處理一些常用的條件判別。搭配上之前所學到的 function pattern matching,能讓程式更易於閱讀與維護。我們先看一個簡單的例子:

defmodule Math do
  def abs(i) when i < 0, do: -i
  def abs(i), do: i
end

Guards 就是在函式宣告的參數括號後方,加上 when 及一個回傳布林值的 expression。在這個例子中,我們在第一個函式區塊判斷傳入的 i 是否小於 1。而當 guards 判別回傳 false 時,就會向下繼續尋找可用的函式區塊。

Guards 裡可以放的表達式

由於有編譯及執行速度上的考量,guards 只能用限定的函式及操作符,包括四則運算、大小判斷如 ==><,型別判斷如 is_listis_boolean 等,更詳細的清單請參考官網的詳細列表

特別要注意的是在 guards 裡不能呼叫自定義的一般函式。目前若需要自定義 guard,需要用進階的 defmacro ,並在該 macro 中只用到上述清單中允許的函式。而且除了檢查並回傳布林值之外,不能有其它的副作用。而新版的 elixir 中也將會提供 defguard 用來自訂義 guard,並在編譯期進行檢查。

可以使用 guards 的地方

Guards 可以用在三個地方,分別是具名函式及匿名函式宣告,及 case 表達式裡,會在相應的介紹裡說明。

一個函式區塊串接多個 guards

若要在同個函式區塊中使用多個 guards,要用 and 或是 orand 就是必須符合所有的條件,而 or 則是只要任一條件符合即可。

  def is_even(n) when is_integer(n) and rem(n, 2) == 0, 
    do: true
  def is_even(_other),
    do: false

  def is_number(term)
    when is_integer(term) 
    or is_float(term),
    do: true

  def is_number(_other),
    do: false

另外依程式可讀性需求,你也可以把 or 改成 when

def is_number(term)
  when is_integer(term)
  when is_float(term),
  do: true

def is_number(_other),
  do: false

Pipe operator: |>

不管是寫哪種程式語言,只看程式的形狀,你一定做過類似這樣的事情:

request = generate_request
response = get_response(request)
body = parse_body(response, :html)
html = render(body)

但仔細想想,當你最終想要的只有那個 html 的結果,那這個例子中的 requestresponsebody 都是沒有用的臨時變數。想要去掉這些變數,你可以這樣寫:

html = render(parse_body(get_response(generate_request), :html))

然後你就被同事記恨了。因為這看起來更醜,得要從裡面讀到外面,而且還常常找不到開始讀的點。大概只有寫 LISP 的人才看得習慣。

Elixir 的 pipe operator |> 讓你把 expression 的結果當成下一個函式呼叫的 第一個參數,所以上面的例子可以改寫成:

html = 
  generate_request
  |> get_response
  |> parse_body(:html)
  |> render

再來一點點理論

在數學中,這個行為叫做 function composition,用 Haskell 為例,當你想要得到以下式子的結果:

y = f(g(h(x)))

你想要的,就是 f, gh複合函數,在 Haskell 中,你可以這樣寫:

y = f.g.h x

但雖然省掉了臨時變數跟括號, function composition 的寫法依然是由內而外的。Elixir 的 pipe operator 則是符合執行順序的:

y = x |> h |> g |> f

著名的 JavaScript FP 函式庫 Ramda 及 Lodash/fp 都提供了兩種方向的函式。Ramda 裡是 R.composeR.pipe, 在 lodash/fp 裡叫做 fp.composefp.pipe

無負擔亂印執行過程

記得我們之前為了取得執行的結果,做了這樣的事:

y = Math.add_one(10)
IO.inspect(y)

你可以把該例子改成這樣:

Math.add_one(10)
|> IO.inspect

IO.inspect 不同於專用於字串列印的 IO.puts,它會將接收到的參數印出來,並將結果繼續向下傳。所以你還可以這樣做:

html = 
  generate_request
  |> IO.inspect
  |> get_response
  |> IO.inspect
  |> parse_body(:html)
#  |> IO.inspect
  |> render
  |> IO.inspect

不想印某些結果時,只要像上面把該行註解掉就好。

Pipe operator 是 Elixir 裡最著名也最重要的操作符,請務必要熟悉它的用法。

重點回顧

  • 在函式宣告中,可以用 when 幫函式加上 guards ,直接對值進行檢查
  • Guards 裡只能呼叫特定的函式。
  • 自訂 guards 需要使用進階的 macro 技巧
  • Guards 可以用在具名函式、匿名函式及 case 表達式中
  • Pipe operator |> 會將表達式的結果,當做下一個函式呼叫的第一個參數
  • IO.inspect 會列印及回傳接收到的參數

Happy hacking! 明天見。


上一篇
親愛的,遞迴把記憶體塞爆了
下一篇
匿名函式
系列文
函數式編程: 從 Elixir & Phoenix 入門。31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
dannyh79
iT邦新手 5 級 ‧ 2019-12-13 00:02:57

文章中第一個範例的 defmodule 的尾巴是不是少了個 do 。 qq

defmodule Math
  def abs(i) when i < 0, do: -i
  def abs(i), do: i
end
taiansu iT邦新手 4 級 ‧ 2019-12-23 17:56:30 檢舉

啊,是的。感謝修正。

我要留言

立即登入留言